// forked from cue's "HTML5で3D その6（ドーナツ）" http://jsdo.it/cue/pVGK
// forked from cue's "HTML5で3D その5（プリミティブ）" http://jsdo.it/cue/mxgb
// forked from cue's "HTML5で3D その4（Zソート）" http://jsdo.it/cue/xniI
// forked from cue's "HTML5で3D その3（軸変換）" http://jsdo.it/cue/oFin
/**
 * HTML5で3D
 * ベンチマークはやっぱりドーナツで
 * うーん陰面消去が正確な判別じゃないからガビガビする・・
 * あとなんかカクカクするなぁ。僕のソースが悪いんだろうな。
 */

// 頂点
function Vertex3D(x, y, z) {
  // 3D座標
  this.x = x || 0;
  this.y = y || 0;
  this.z = z || 0;
  
  // 変換後の座標
  this.tx = this.x;
  this.ty = this.y;
  this.tz = this.z;
  
  // 2D座標
  this.screenX = 0;
  this.screenY = 0;
}

// カメラ
function Camera(x, y, z, angle) {
  Vertex3D.apply(this, [x, y, z]);
  this._angle = 0;
  this.fov = 0;
  this.angle = function(value) {
    if(value) {
      this._angle = value;
      this.fov = 1 / Math.tan(this._angle * 0.5 * Math.PI / 180) * canvas.width * 0.5;
    }
    else {
      return this._angle;
    }
  };
  this.angle(angle || 60);
}
Camera.prototype = new Vertex3D;

// マテリアル
function Material3D(fillColor, fillAlpha, lineColor, lineAlpha, lineThickness) {
  this.fillColor = fillColor || "#000000";
  this.fillAlpha = fillAlpha || 0;
  this.lineColor = lineColor || null;
  this.lineAlpha = lineAlpha || 0;
  this.lineThickness = lineThickness || 1;
  this.doubleSide = false;
}

// 面
function Face3D(indecies) {
  this.indecies = indecies;
  this.screenZ = 0;
}

function DisplayObject3D(material) {
  this.vertecies = [];
  this.faces = [];
  
  this._rotationX = 0;
  this._rotationY = 0;
  this._rotationZ = 0;
  this._rotated = true;
  
  // Zソート用
  this.screenZ = 0;
  // 塗り
  this.material= material || new Material3D();
  this.renderedFaces = 0;
  
  this.rotationX = function(value) {
    if(value) { this._rotationX = value; this._rotated = true; }
    else      { return this._rotationX; }
  };
  this.rotationY = function(value) {
    if(value) { this._rotationY = value; this._rotated = true; }
    else      { return this._rotationY; }
  };
  this.rotationZ = function(value) {
    if(value) { this._rotationZ = value; this._rotated = true; }
    else      { return this._rotationZ; }
  };
  this.addVertex = function(vertex) {
    for(var i = 0; i < arguments.length; i++) {
      this.vertecies.push(arguments[i]);
    }
  };
  this.removeVertex = function(vertex) {
    for(var i = 0, l = this.vertecies.length; i < l; i++) {
      if(this.vertecies[i] == vertex) {
        this.vertecies.splice(i, 1);
        return;
      }
    }
  };
  this.addFace = function() {
    for(var i = 0; i < arguments.length; i++) {
      this.faces.push(arguments[i]);
    }
  };
  this.removeFaces = function(face) {
    for(var i = 0, l = this.faces.length; i < l; i++) {
      if(this.faces[i] == face) {
        this.faces.splice(i, 1);
        return;
      }
    }
  };
  this.project = function(camera) {
    if(this._rotated) {
      this.updateRotation(camera);
    }
    
    for(var i = 0, l = this.vertecies.length; i < l; i++) {
      var vertex = this.vertecies[i];
      var perseScale = camera.fov / ( camera.fov + vertex.tz );
      vertex.screenX = vertex.tx * perseScale + canvas.width * 0.5;
      vertex.screenY = vertex.ty * perseScale + canvas.height * 0.5;
      vertex.screenZ = vertex.tz;
    }
    var fz = 0;
    for(i = 0, l = this.faces.length; i < l; i++) {
      var faceZ = 0;
      for(var j = 0, m = this.faces[i].indecies.length; j < m; j++) {
        faceZ += this.vertecies[this.faces[i].indecies[j]].screenZ;
     }
      
      faceZ = faceZ / m;
      fz += faceZ;
      this.faces[i].screenZ = faceZ;
    }
    this.screenZ = fz / l;
  };
  this.render = function(context) {
    this.faces = this.faces.sort(function(a, b) { return b.screenZ - a.screenZ; });
    this.renderedFaces = 0;
    for(var i = 0, l = this.faces.length; i < l; i++) {
      if(this.faces[i].indecies.length < 3) { return; }
      var v0 = this.vertecies[this.faces[i].indecies[0]];
      var v1 = this.vertecies[this.faces[i].indecies[1]];
      var v2 = this.vertecies[this.faces[i].indecies[2]];
      
      
      // 両面描画もしくは背面以外
      if(this.material.doubleSide || ((v1.tx - v0.tx) * (v2.ty - v0.ty) - (v1.ty - v0.ty) * (v2.tx - v0.tx)) < 1) {
        var ax = v1.tx - v0.tx;
        var ay = v1.ty - v0.ty;
        var az = v1.tz - v0.tz;
        var bx = v2.tx - v1.tx;
        var by = v2.ty - v1.ty;
        var bz = v2.tz - v1.tz;
        
        var nx = ay * bz - az * by;
        var ny = az * bx - ax * bz;
        var nz = ax * by - ay * bx;
        
        // ノーマライズ
        var nLen = Math.sqrt( nx * nx + ny * ny + nz * nz );
        
        var fac = Math.acos(nz / nLen) / Math.PI;
        var rgb, r, g, b;
        if(this.material.fillColor) {
          rgb = /^#(\w{2})(\w{2})(\w{2})$/.exec(this.material.fillColor);
          r = parseInt("0x"+rgb[1], 16);
          g = parseInt("0x"+rgb[2], 16);
          b = parseInt("0x"+rgb[3], 16);
          context.fillStyle = "rgba(" + parseInt(r * fac, 10) + ", " + parseInt(g * fac, 10) + ", " + parseInt(b * fac, 10) + ", " + this.material.fillAlpha + ")";
        }
        if(this.material.lineColor) {
          rgb = /^#(\w{2})(\w{2})(\w{2})$/.exec(this.material.lineColor);
          r = parseInt("0x"+rgb[1], 16);
          g = parseInt("0x"+rgb[2], 16);
          b = parseInt("0x"+rgb[3], 16);
          context.strokeStyle = "rgba(" + parseInt(r, 10) + ", " + parseInt(g, 10) + ", " + parseInt(b, 10) + ", " + this.material.fillAlpha + ")";
        }
        context.beginPath();
        context.moveTo(this.vertecies[this.faces[i].indecies[0]].screenX, this.vertecies[this.faces[i].indecies[0]].screenY);
        for(var j = 1, m = this.faces[i].indecies.length; j < m; j++) {
          context.lineTo(this.vertecies[this.faces[i].indecies[j]].screenX, this.vertecies[this.faces[i].indecies[j]].screenY);
        }
        context.lineTo(this.vertecies[this.faces[i].indecies[0]].screenX, this.vertecies[this.faces[i].indecies[0]].screenY);
        context.closePath();
        if(this.material.fillColor) { context.fill(); }
        if(this.material.lineColor) { context.stroke(); }
        this.renderedFaces++;
      }
    }
  };
  this.updateRotation = function(camera) {
    var cosX = Math.cos( this._rotationX );
    var sinX = Math.sin( this._rotationX );
    var cosY = Math.cos( this._rotationY );
    var sinY = Math.sin( this._rotationY );
    var cosZ = Math.cos( this._rotationZ );
    var sinZ = Math.sin( this._rotationZ );
    
    // 軸変換
    // あれ？カメラ座標ってここでいいんかいな？
    var tx, ty, tz;
    for(var i = 0, l = this.vertecies.length; i < l; i++) {
      var vertex = this.vertecies[i];
      var x = vertex.x;
      var y = vertex.y;
      var z = vertex.z;
      var temp = 0;
      
      ty = y * cosX - z * sinX;
      tz = y * sinX + z * cosX;
      y = ty;
      z = tz;
      
      tx = x * cosY + z * sinY;
      tz = z * cosY - x * sinY;
      x = tx;
      z = tz;
      
      vertex.tx = x - camera.x;
      vertex.ty = y - camera.y;
      vertex.tz = z - camera.z;
    }
  };
}

// ドーナツ
function Donuts(material, size, seguments)
{
  DisplayObject3D.apply(this, [material]);
  size = size || 2;
  seguments = seguments || 16;
  var i, j;
  var rRad = 0;
  var cRad = 0;
  var t = Math.PI * 2 / seguments;
  var vn = 0;
  var r = 2;
  i = 0;
  while (i < seguments) {
    var cS = Math.sin(cRad) + r;
    var cC = Math.cos(cRad);
    j = 0;
    while (j < seguments) {
      var rS = Math.cos(rRad);
      var rC = Math.sin(rRad);
      this.addVertex(new Vertex3D(rS * cS * size, rC * cS * size, cC * size));
      rRad = rRad + Math.PI / seguments * 2;
      vn++;
      j++;
    }
    cRad += t;
    i++;
  }
  
  i = 0;
  var p = seguments * seguments;
  j = 0;
  while (i < seguments) {
    t = i * seguments;
    j = 0;
    while (j < seguments) {
      this.addFace(
        new Face3D([
          (t + seguments) % p + (j + 1) % seguments,
          t + (j + 1) % seguments,
          t + j,
          (t + seguments) % p + j
        ])
      );
      j++;
    }
    i++;
  }
}
Donuts.prototype = new DisplayObject3D;


var canvas = document.getElementById('world');
var cxt = canvas.getContext("2d");

// カメラ
var camera = new Camera(0, 0, -200);

var segment = 8;
var material = new Material3D("#0000FF", 1);
var donuts = new Donuts(material, 80, segment);
donuts.rotationY(15 * Math.PI / 180);

//var stats = new Stats();
//stats.domElement.style.position = 'absolute';
//stats.domElement.style.top = '0px';
//document.body.appendChild(stats.domElement);

function render() {
  cxt.clearRect(0, 0, 465, 465);
  
  donuts.project(camera);
  donuts.render(cxt);
  
  cxt.fillStyle = "#000000";
  cxt.fillText("Segments: " + segment, 5, 60);
  cxt.fillText("Vertecies: " + donuts.vertecies.length, 5, 72);
  cxt.fillText("Faces: " + donuts.faces.length, 5, 84);
  cxt.fillText("Render Face: " + donuts.renderedFaces, 5, 96);
  
  //stats.update();
}


var _rx = donuts.rotationX();
var _ry = donuts.rotationY();
var _rz = donuts.rotationZ();
function enterFrame() {
  donuts.rotationX(_rx);
  donuts.rotationY(_ry);
  donuts.rotationZ(_rz);
  render();
  _rx += 0.04;
  _ry += 0.06;
  _rz += 0.02;
}

function addSegment(s) {
  segment = Math.max(segment + s, 4);
  donuts = new Donuts(material, 80, segment);
  donuts.rotationX(_rx);
  donuts.rotationY(_ry);
  donuts.rotationZ(_rz);
  render();
}

function changeMaterial() {
  if(document.getElementById('fillRender').checked) {
    var fillColSel = document.getElementById('fillCol');
    var fillCol = fillColSel.options[fillColSel.selectedIndex].value;
    donuts.material.fillColor = fillCol;
  }
  else {
    donuts.material.fillColor = null;
  }
  donuts.material.lineColor = (document.getElementById('wireRender').checked) ? "#FF0000" : null;
  donuts.material.doubleSide = (document.getElementById('doubleSide').checked);
  render();
}



setInterval(enterFrame, 1000 / 60);
render();

